package mh.map;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;

import mh.entity.Entity;
import mh.map.objectset.ObjectSet;
import mh.map.render.Render;

/**
 * Includes a static referenc to each {@link TileSet}, {@link ObjectSet}, {@link Render} or {@link Entity}, create since
 * the start of the game. It has a timeout expiration for each object of 10 minutes, if the object is not used for that
 * long it is removed from the cache. For ObjectSet and TileSet it works with imageName.
 * 
 * @author Gianmarco Laggia
 * @author Alberto D'Este
 * 
 */
public class CachedResources extends Thread {

	/**
	 * The logger
	 */
	private static final Logger					LOG						= Logger.getLogger(CachedResources.class.getName());

	/** Contains a reference to all cached resources with the time they were cached. */
	private final HashMap<Object, Long>			cache					= new HashMap<Object, Long>();

	private final HashMap<String, ObjectSet>	objectsetsFromName		= new HashMap<String, ObjectSet>();
	private final ArrayList<ObjectSet>			objectsets				= new ArrayList<ObjectSet>();
	private final TreeMap<Integer, ObjectSet>	objectsetsForIdInside	= new TreeMap<Integer, ObjectSet>();

	private final HashMap<String, TileSet>		tilesetsFromName		= new HashMap<String, TileSet>();
	private final ArrayList<TileSet>			tilesets				= new ArrayList<TileSet>();

	/** Time limit for a cached object to remain cached. (10 minutes) */
	private static final long					EXPIRING_TIME_MS		= 600000;

	/** The single instance of this class to ever exist (singleton) */
	private static final CachedResources		single					= new CachedResources();

	/**
	 * @return The single instance of this class (singleton)
	 */
	public static CachedResources singleton() {
		return single;
	}

	private CachedResources() {
	}

	/**
	 * Cache a new Object or replace a previously cached one. Caching works on Image names, it's nonsense to have two
	 * different Tileset or Objectset with the same image name.
	 * 
	 * @param o
	 *            The Object to be added. Name is univoke.
	 */
	public void store(Object o) {
		if (!single.isAlive()) {
			System.out.println("Caching problems, impossible to delete expired resources. This features will not be avaible until next restart of the game.");
		}
		if (o instanceof ObjectSet) {
			ObjectSet os = (ObjectSet) o;
			if (!this.objectsetsFromName.containsKey(os.getImgSource())) {
				this.cache.put(os, System.currentTimeMillis());
				this.objectsets.add(os);
				this.objectsetsFromName.put(os.getImgSource(), os);
			} else {
				this.remove(os);
				this.store(os);
			}
		}
		if (o instanceof TileSet) {
			TileSet ts = (TileSet) o;
			if (!this.tilesetsFromName.containsKey(ts.getImgSource())) {
				this.cache.put(ts, System.currentTimeMillis());
				this.tilesets.add(ts);
				this.tilesetsFromName.put(ts.getImgSource(), ts);
			} else {
				this.remove(ts);
				this.store(ts);
			}
		}
	}

	/**
	 * When a resource complete its load process tells the resource handler what contains
	 * 
	 * @param o
	 *            The calling ObjectSet
	 * @param keys
	 *            List of object inside the ObjectSet
	 */
	public void loaded(ObjectSet o, Set<Integer> keys) {
		for (int i : keys) {
			this.objectsetsForIdInside.put(i, o);
		}
	}

	/**
	 * Removes a cached object
	 * 
	 * @param o
	 *            The object to be removed.
	 */
	public void remove(Object o) {
		if (o instanceof ObjectSet) {
			ObjectSet os = (ObjectSet) o;
			if (this.objectsetsFromName.containsKey(os.getImgSource())) {
				ObjectSet toRemove = this.objectsetsFromName.remove(os.getImgSource());
				this.cache.remove(toRemove);
				this.objectsets.remove(toRemove);
			}
			return;
		}
		if (o instanceof TileSet) {
			TileSet ts = (TileSet) o;
			if (this.tilesetsFromName.containsKey(ts.getImgSource())) {
				TileSet toRemove = this.tilesetsFromName.remove(ts.getImgSource());
				this.cache.remove(toRemove);
				this.tilesets.remove(toRemove);
			}
			return;
		}
	}

	/**
	 * @param imgName
	 *            The <code>ObjectSet</code> or <code>TileSet</code> to find.
	 * @return The correct object or <code>null</code> otherwise.
	 */
	public Object get(String imgName) {
		if (this.objectsetsFromName.containsKey(imgName)) {
			ObjectSet o = this.objectsetsFromName.get(imgName);
			this.cache.remove(o);
			this.cache.put(o, System.currentTimeMillis());
			return o;
		}
		if (this.tilesetsFromName.containsKey(imgName)) {
			TileSet t = this.tilesetsFromName.get(imgName);
			this.cache.remove(t);
			this.cache.put(t, System.currentTimeMillis());
			return t;
		}
		return null;
	}

	/**
	 * Refreshes the expiration time of a already present object or add it.
	 * 
	 * @param o
	 *            The object to refresh
	 * @return True if it was present
	 */
	public boolean hearthBeat(Object o) {
		if (this.cache.containsKey(o)) {
			this.cache.put(o, System.currentTimeMillis());
			return true;
		}
		return false;
	}

	/**
	 * @param id
	 * @return The objectset that contains the specified ID
	 */
	public ObjectSet getObjectSet(int id) {
		return this.objectsetsForIdInside.get(id);
	}

	@Override
	public void run() {
		while (true) {
			LOG.config("UPDATING CACHE");
			long actualTime = System.currentTimeMillis();
			for (ObjectSet o : this.objectsets) {
				long cachingMoment = this.cache.get(o);
				if ((actualTime - cachingMoment) > EXPIRING_TIME_MS) {
					this.remove(o);
				}
			}
			for (TileSet t : this.tilesets) {
				long cachingMoment = this.cache.get(t);
				if ((actualTime - cachingMoment) > EXPIRING_TIME_MS) {
					this.remove(t);
				}
			}
			try {
				Thread.sleep(30000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
